/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.logiclander.ricemine.services.identity;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.kuali.rice.kim.bo.entity.dto.KimEntityAddressInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityAffiliationInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityDefaultInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityEmailInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityEmploymentInformationInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityEntityTypeDefaultInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityEntityTypeInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityExternalIdentifierInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityNameInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityNamePrincipalNameInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityPhoneInfo;
import org.kuali.rice.kim.bo.entity.dto.KimEntityPrivacyPreferencesInfo;
import org.kuali.rice.kim.bo.entity.dto.KimPrincipalInfo;
import org.kuali.rice.kim.bo.reference.dto.AddressTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.AffiliationTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.CitizenshipStatusInfo;
import org.kuali.rice.kim.bo.reference.dto.EmailTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.EmploymentStatusInfo;
import org.kuali.rice.kim.bo.reference.dto.EmploymentTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.EntityNameTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.EntityTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.ExternalIdentifierTypeInfo;
import org.kuali.rice.kim.bo.reference.dto.PhoneTypeInfo;
import org.kuali.rice.kim.service.IdentityService;
import org.kuali.rice.kns.util.ObjectUtils;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.ldap.filter.NotFilter;
import org.springframework.ldap.filter.OrFilter;

/**
 *
 * @author tcarroll
 */
public class LdapIdentityServiceImpl implements IdentityService {

  private final Log log = LogFactory.getLog(getClass());
  protected LdapOperations ldapContext;
  protected String searchBase;
  protected String searchFilter;
  private LdapKimPrincipalInfoMapper ldapKimPrincipalInfoMapper;
  private LdapKimEntityInfoMapper ldapKimEntityInfoMapper;
  private Map ldapKimAttributeMapping;

//beg: methods required by IdentityService interface
  public KimPrincipalInfo getPrincipal(String principalId) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimPrincipalInfoMapper.getPrincipalIdAttributeName(), principalId);
    return lookup(KimPrincipalInfo.class, this.ldapKimPrincipalInfoMapper, criteria);
  }

  public KimPrincipalInfo getPrincipalByPrincipalName(String principalName) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimPrincipalInfoMapper.getPrincipalNameAttributeName(), principalName);
    return lookup(KimPrincipalInfo.class, this.ldapKimPrincipalInfoMapper, criteria);
  }

  public KimPrincipalInfo getPrincipalByPrincipalNameAndPassword(String principalName, String password) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public KimEntityDefaultInfo getEntityDefaultInfo(String entityId) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimEntityInfoMapper.getEntityIdAttributeName(), entityId);
    KimEntityInfo kei = lookup(KimEntityInfo.class, this.ldapKimEntityInfoMapper, criteria);
    return getEntityDefaultInfo(kei);
  }

  public KimEntityDefaultInfo getEntityDefaultInfoByPrincipalId(String principalId) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public KimEntityDefaultInfo getEntityDefaultInfoByPrincipalName(String principalName) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public KimEntityInfo getEntityInfo(String entityId) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimEntityInfoMapper.getEntityIdAttributeName(), entityId);
    KimEntityInfo kei = lookup(KimEntityInfo.class, this.ldapKimEntityInfoMapper, criteria);
    return kei;
  }

  public KimEntityInfo getEntityInfoByPrincipalId(String principalId) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimPrincipalInfoMapper.getPrincipalIdAttributeName(), principalId);
    KimEntityInfo kei = lookup(KimEntityInfo.class, this.ldapKimEntityInfoMapper, criteria);
    return kei;
  }

  public KimEntityInfo getEntityInfoByPrincipalName(String principalName) {
    Map<String, Object> criteria = new HashMap();
    criteria.put(ldapKimPrincipalInfoMapper.getPrincipalNameAttributeName(), principalName);
    KimEntityInfo kei = lookup(KimEntityInfo.class, this.ldapKimEntityInfoMapper, criteria);
    return kei;
  }

  public List<KimEntityDefaultInfo> lookupEntityDefaultInfo(Map<String, String> searchCriteria, boolean unbounded) {
    Map<String, Object> criteria = mapLdapLookupCriteria(searchCriteria);
    List<KimEntityInfo> results = search(KimEntityInfo.class, this.ldapKimEntityInfoMapper, criteria);
    return getEntityDefaultInfo(results);
  }

  public List<KimEntityInfo> lookupEntityInfo(Map<String, String> searchCriteria, boolean unbounded) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public int getMatchingEntityCount(Map<String, String> searchCriteria) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public KimEntityPrivacyPreferencesInfo getEntityPrivacyPreferences(String entityId) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public Map<String, KimEntityNamePrincipalNameInfo> getDefaultNamesForPrincipalIds(List<String> principalIds) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public Map<String, KimEntityNameInfo> getDefaultNamesForEntityIds(List<String> entityIds) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public AddressTypeInfo getAddressType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public AffiliationTypeInfo getAffiliationType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public CitizenshipStatusInfo getCitizenshipStatus(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public EmailTypeInfo getEmailType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public EmploymentStatusInfo getEmploymentStatus(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public EmploymentTypeInfo getEmploymentType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public EntityNameTypeInfo getEntityNameType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public EntityTypeInfo getEntityType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public ExternalIdentifierTypeInfo getExternalIdentifierType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  public PhoneTypeInfo getPhoneType(String code) {
    throw new UnsupportedOperationException("Not supported yet.");
  }
//end: methods required by IdentityService interface
//beg: configuration

  public LdapOperations getLdapContext() {
    return this.ldapContext;
  }

  public void setLdapContext(LdapOperations ldapContext) {
    this.ldapContext = ldapContext;
  }

  public String getSearchBase() {
    return this.searchBase;
  }

  public void setSearchBase(String searchBase) {
    this.searchBase = searchBase;
  }

  public String getSearchFilter() {
    return this.searchFilter;
  }

  public void setSearchFilter(String searchFilter) {
    this.searchFilter = searchFilter;
  }

  public LdapKimPrincipalInfoMapper getLdapKimPrincipalInfoMapper() {
    return this.ldapKimPrincipalInfoMapper;
  }

  public void setLdapKimPrincipalInfoMapper(LdapKimPrincipalInfoMapper ldapKimPrincipalInfoMapper) {
    this.ldapKimPrincipalInfoMapper = ldapKimPrincipalInfoMapper;
  }

  public LdapKimEntityInfoMapper getLdapKimEntityInfoMapper() {
    return this.ldapKimEntityInfoMapper;
  }

  public void setLdapKimEntityInfoMapper(LdapKimEntityInfoMapper ldapKimEntityInfoMapper) {
    this.ldapKimEntityInfoMapper = ldapKimEntityInfoMapper;
  }

  public Map getLdapKimAttributeMapping() {
    return ldapKimAttributeMapping;
  }

  public void setLdapKimAttributeMapping(Map ldapKimAttributeMapping) {
    this.ldapKimAttributeMapping = ldapKimAttributeMapping;
  }
//end: configuration
//beg: helpers
  protected List<KimEntityDefaultInfo> getEntityDefaultInfo(List<KimEntityInfo> entities) {
    List<KimEntityDefaultInfo> kedis = new ArrayList();
    for (Iterator<KimEntityInfo> iter = entities.iterator(); iter.hasNext();) {
      KimEntityInfo kei = iter.next();
      kedis.add(getEntityDefaultInfo(kei));
    }
    return kedis;
  }

  protected KimEntityDefaultInfo getEntityDefaultInfo(KimEntityInfo entity) {
    if (log.isDebugEnabled()) {
      log.debug("extractDefaultInfo(input): entityId: " + entity.getEntityId()
              + ", firstName: " + entity.getDefaultName().getFirstName()
              + ", middleName: " + entity.getDefaultName().getMiddleName()
              + ", lastName: " + entity.getDefaultName().getLastName()
              + ", formattedName: " + entity.getDefaultName().getFormattedName());
    }
    KimEntityDefaultInfo dflt = new KimEntityDefaultInfo();
    dflt.setEntityId(entity.getEntityId());
    dflt.setActive(entity.isActive());
    ArrayList<KimPrincipalInfo> principalInfo = new ArrayList<KimPrincipalInfo>(entity.getPrincipals().size());
    dflt.setPrincipals(principalInfo);
    for (KimPrincipalInfo p : entity.getPrincipals()) {
      principalInfo.add(new KimPrincipalInfo(p));
    }
    KimEntityPrivacyPreferencesInfo privacy = null;
    if (ObjectUtils.isNotNull(entity.getPrivacyPreferences())) {
      privacy = new KimEntityPrivacyPreferencesInfo(entity.getPrivacyPreferences());
    }

    dflt.setPrivacyPreferences(privacy);
    dflt.setDefaultName(new KimEntityNameInfo(entity.getDefaultName()));
    ArrayList<KimEntityEntityTypeDefaultInfo> entityTypesInfo = new ArrayList<KimEntityEntityTypeDefaultInfo>(entity.getEntityTypes().size());
    dflt.setEntityTypes(entityTypesInfo);
    for (KimEntityEntityTypeInfo entityEntityType : entity.getEntityTypes()) {
      KimEntityEntityTypeDefaultInfo typeInfo = new KimEntityEntityTypeDefaultInfo();
      typeInfo.setEntityTypeCode(entityEntityType.getEntityTypeCode());
      typeInfo.setDefaultAddress(new KimEntityAddressInfo(entityEntityType.getDefaultAddress()));
      typeInfo.setDefaultEmailAddress(new KimEntityEmailInfo(entityEntityType.getDefaultEmailAddress()));
      typeInfo.setDefaultPhoneNumber(new KimEntityPhoneInfo(entityEntityType.getDefaultPhoneNumber()));
      entityTypesInfo.add(typeInfo);
    }
    ArrayList<KimEntityAffiliationInfo> affInfo = new ArrayList<KimEntityAffiliationInfo>(entity.getAffiliations().size());
    dflt.setAffiliations(affInfo);
    for (KimEntityAffiliationInfo aff : entity.getAffiliations()) {
      affInfo.add(new KimEntityAffiliationInfo(aff));
      if (aff.isActive() && aff.isDefault()) {
        dflt.setDefaultAffiliation(affInfo.get(affInfo.size() - 1));
      }
    }
    dflt.setPrimaryEmployment(new KimEntityEmploymentInformationInfo(entity.getPrimaryEmployment()));
    ArrayList<KimEntityExternalIdentifierInfo> idInfo = new ArrayList<KimEntityExternalIdentifierInfo>(entity.getExternalIdentifiers().size());
    dflt.setExternalIdentifiers(idInfo);
    for (KimEntityExternalIdentifierInfo id : entity.getExternalIdentifiers()) {
      idInfo.add(new KimEntityExternalIdentifierInfo(id));
    }
    if (log.isDebugEnabled()) {
      if (dflt != null) {
        log.debug("getDefaultInfo(output): entityId: " + dflt.getEntityId()
                + ", firstName: " + dflt.getDefaultName().getFirstName()
                + ", middleName: " + dflt.getDefaultName().getMiddleName()
                + ", lastName: " + dflt.getDefaultName().getLastName()
                + ", formattedName: " + dflt.getDefaultName().getFormattedName());
      } else {
        log.debug("extractDefaultInfo: entity is null");
      }
    }
    return dflt;
  }

  private Map<String, Object> mapLdapLookupCriteria(Map<String, String> searchCriteria) {
    Map<String, Object> criteria = new HashMap();
    for (Map.Entry<String, String> criteriaEntry : searchCriteria.entrySet()) {
      log.debug("mapping criteria: " + criteriaEntry.getKey() + "=" + criteriaEntry.getValue());
      if (ldapKimAttributeMapping.containsKey(criteriaEntry.getKey())) {
        criteria.put((String) ldapKimAttributeMapping.get(criteriaEntry.getKey()),
                criteriaEntry.getValue());
        log.debug("match (" + criteriaEntry.getKey() + "): " + (String) ldapKimAttributeMapping.get(criteriaEntry.getKey()) + "=" + criteriaEntry.getValue());
      } else {
        log.debug("no match (" + criteriaEntry.getKey() + ")");
      }
    }
    return criteria;
  }

  public <T> List<T> search(Class<T> type, ContextMapper mapper, Map<String, Object> criteria) {
    if (log.isDebugEnabled()) {
      log.debug("generic search called!");
    }
    AndFilter filter = new AndFilter();
    for (Map.Entry<String, Object> entry : criteria.entrySet()) {
      if (entry.getValue() instanceof Iterable) {
        OrFilter orFilter = new OrFilter();
        for (String value : (Iterable<String>) entry.getValue()) {
          orFilter.or(new LikeFilter(entry.getKey(), value));
        }
        orFilter.or(new NotFilter(new LikeFilter(entry.getKey(), "*")));
        filter.and(orFilter);
      } else {
        filter.and(new LikeFilter(entry.getKey(), (String) entry.getValue()));
      }
    }
    log.debug("search filter: " + filter);
    List<T> result = this.ldapContext.search(searchBase, filter.encode(), mapper);
    return result;
  }

  public <T> T lookup(Class<T> type, ContextMapper mapper, Map<String, Object> criteria) {
    if (log.isDebugEnabled()) {
      log.debug("generic lookup called!");
    }
// potential to be special or synthetic, so do some special processing
//    List<T> result = new LinkedList();
//    if (name == null || "".equals(name) || value == null || "".equals(value)) {
//      if (log.isDebugEnabled()) {
//        log.debug("cannot complete the entity search: name or value is null or empty.");
//      }
//      result.add(null);
//    } else if (name.equalsIgnoreCase("dn")) {
//      if (log.isDebugEnabled()) {
//        log.debug("entity search for: name=" + name + " and value=" + value + " and requireUnique=" + requireUnique);
//      }
//      result.add((KimEntityInfo) this.ldapContext.lookup(value, mapper));
//    } else if (name.equals("objectGUID")) {
//      if (log.isDebugEnabled()) {
//        log.debug("entity search for: name=" + name + " and value=" + value + " and requireUnique=" + requireUnique);
//      }
//      StringBuffer qstr = new StringBuffer("(&");
//      if (!(null == searchFilter || "".equals(searchFilter))) {
//        qstr.append(searchFilter);
//      }
//
//      // todo: need to reverse engineer the buildGuid(context) process to get the key to search for principalId
//      // in place of this: qstr.append(String.format("(%1$s=%2$s)", name, value));
//
//      qstr.append(")");
//
//      result = this.ldapContext.search(searchBase, qstr.toString(), mapper);
//
//    } else {
//      if (log.isDebugEnabled()) {
//        log.debug("entity search for: name=" + name + " and value=" + value + " and requireUnique=" + requireUnique);
//      }
//      StringBuffer qstr = new StringBuffer("(&");
//      if (!(null == searchFilter || "".equals(searchFilter))) {
//        qstr.append(searchFilter);
//      }
//      qstr.append(String.format("(%1$s=%2$s)", name, value));
//      qstr.append(")");
//
//      result = this.ldapContext.search(searchBase, qstr.toString(), mapper);
//    }
    List<T> result = search(type, mapper, criteria);
    if (result.size() > 1) {
      if (log.isDebugEnabled()) {
        log.debug("result size=" + result.size());
      }
      result.clear();
      result.add(null);
    }
    return result.get(0);
  }
  //end: helpers
}
